package org.testfire.realm.servlet.ajax.util;

/**
 * Created by IntelliJ IDEA.
 * User: granum
 * Date: Aug 20, 2007
 * Time: 10:37:46 AM
 */

import java.lang.reflect.Field;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * JSON: Java Script Object Notation.
 */
public class JsonMarshaler
{

    /* How deeply into an object to recurse */
    private static final int MAX_RECURSION_DEPTH = 5;

	private JsonMarshaler()
	{
	}

	/**
     * Turn a Map into a JSON object string.
     * @param map The map.
     * @return A JSON formatted version of the map.
     */
    private static String marshalMap( Map map )
    {
        StringBuffer buf = new StringBuffer();

        Set s = map.keySet();
        Iterator it = s.iterator();
        while ( it.hasNext() )
        {
            Object key = it.next();
            Object value = map.get( key );
            String appendVal;
            if ( isPrimative( value ) )
            {
                appendVal = getStringValue( value );
            }
            else
            {
                throw new UnsupportedOperationException(
                    "Only primatives may be marshalled to JSON currently." );
            }
            buf.append( toKeyValuePair( String.valueOf(key), appendVal, true,  it.hasNext() ));
        }
        return buf.toString();
    }

    /**
     * Convert the passed object into a JSON string. If the object has no defined key (
     * e.g. is a primative object) the key will be assumed as 'result'.
     * @param o The object to marshal.
     * @return A JSON formated String.
     * @throws IllegalAccessException -
     */
    public static String marshal( Object o ) throws IllegalAccessException
    {
        if ( isPrimative( o ) )
        {
            return "{\n   'result': " + marshalObject( o, 0 ) + "}\n";
        }
        return "{\n" + marshalObject( o, 0 ) + "}\n";
    }

    /**
     *
     *  @todo Handle lists, arrays and ? other.
     * @param o The Java object to marshal into a JSON object.
     * @param currentDepth The recurssion depth.
     * @return A String representing a JSON object.
     * @throws IllegalAccessException -
     */
    private static String marshalObject( Object o, int currentDepth )
        throws IllegalAccessException
    {
        if ( currentDepth > 5 )
        {
            return "'recursion too deep'";
        }
        if ( isPrimative( o ) )
        {
            return getStringValue( o );
        }
        if ( o instanceof Map )
        {
            return marshalMap( (Map)o );
        }

        StringBuffer buf = new StringBuffer();
        buf.append( "{\n" );
        Field[] fs = o.getClass().getFields();
        for ( int i = 0; i < fs.length; i++ )
        {
            buf.append(toKeyValuePair(
                fs[i].getName(),  marshalObject(fs[i].get( o ), currentDepth + 1 ),
                true, i == fs.length -1)
            );
        }
        buf.append( "\n}" );
        return buf.toString();
    }

    /**
     * Format a key and a value for a JSON item. Utilty method to avoid copy and paste.
     * @param key The key.
     * @param value The value. If a the java script value of the object is a string, it
     * should already be quoted here.
     * @param indent If the key should be indented.
     * @param hasNext If there is another item after this one.
     * @return The key value pair in JSON format.
     */
    private static String toKeyValuePair(String key, String value, boolean indent, boolean hasNext){
        String postFix = hasNext ? ",\n" : "\n";
        return indent ? "    '" + key + "':" + value + postFix
            : "'" + key + "':" + value + postFix;
    }

    /**
     * Convert a Primative object into a value for a key-value pair.
     * @param o The object, which should be a primative.
     * @return The value as a JSON value (strings are quoted).
     */
    private static String getStringValue( Object o )
    {
        String value;
        if ( o == null )
        {
            value = "null";
        }
        else if ( isStringType( o ) )
        {
            // wrap in quotes
            value = "'" + o.toString() + "'";
        }
        else
        {
            value = o.toString();
        }

        return value;
    }

    /**
     * Test if the Java object is to be a Javascript string (javascript Strings must be
     * quoted in JSON).
     * @param o The object to test.
     * @return If the javascript object must be quoted.
     */
    private static boolean isStringType( Object o )
    {
        return o instanceof CharSequence || o instanceof Character;
    }

    /**
     * If the object is a primative type. Because sun chose not to implement from a
     * standard Primative interface.
     * @param o The object.
     * @return If the object is of type int, long, boolean, String, char, etc.
     */
    private static boolean isPrimative( Object o )
    {
        if ( o == null )
        {
            return true;
        }
        String className = o.getClass().getName();
        boolean dotCountPasses = className.lastIndexOf( "." ) == 9;
        return className.startsWith( "java.lang." ) && dotCountPasses;
    }
}

// eof: JsonMarshaler.java
